﻿using System;

//Todo: Document exceptions

namespace NetAssist.Math
{
  /// <summary>
  ///  Represents a Real number that does not lose information when representing Repeating Decimals and Cyclic Numbers.
  /// </summary>
  public class BigReal : IComparable, IEquatable<BigReal>, IComparable<BigReal>
  {
    private long numerator;
    private long denominator;

    private const double MIN_VALUE = 1.0E-19;
    private const double MAX_VALUE = 1.0E+19;
    private const double RATIO_RESOLUTION = 1.0E-13;

    /// <summary>
    /// Gets the numerator portion of the real number.
    /// </summary>
    /// <remarks>This value may differ from the value used to initialize the instance: when initializing a real as 2/6 the Numerator will return the value 1, the Denominator will return 3.</remarks>
    public long Numerator
    {
      get { return this.numerator; }
    }

    /// <summary>
    /// Gets the denominator portion of the real number.
    /// </summary>
    /// <remarks>This value may differ from the value used to initialize the instance: when initializing a real as 2/6 the Denominator will return the value 3, the Numerator will return 1.</remarks>
    public long Denominator
    {
      get { return this.denominator; }
    }

    /// <summary>
    /// Initialize a new real number with int values for Numerator and Denominator.
    /// </summary>
    /// <param name="numerator">The numerator value.</param>
    /// <param name="denominator">The denominator value</param>
    /// <remarks>Values are minimized upon instantiation: 2/6 is converted to 1/3
    public BigReal(int numerator, int denominator)
    {
      Initialize(numerator, denominator);
    }

    /// <summary>
    /// Initialize a new real number with long values for Numerator and Denominator.
    /// </summary>
    /// <param name="numerator">The numerator value.</param>
    /// <param name="denominator">The denominator value</param>
    /// <remarks>Values are minimized upon instantiation: 2/6 is converted to 1/3
    public BigReal(long numerator, long denominator)
    {
      Initialize(numerator, denominator);
    }

    /// <summary>
    /// Initialize a new real number from a double precision floating point value.
    /// </summary>
    /// <param name="value">The floating point value to use.</param>
    /// <remarks>Some values may not convert properly: Repeating Decimals may have a small error when converting.</remarks>
    public BigReal(double value)
    {
      Initialize(value);
    }

    /// <summary>
    /// Initialize a new real number from a single precision floating point value.
    /// </summary>
    /// <param name="value">The floating point value to use.</param>
    /// <remarks>Some values may not convert properly: Repeating Decimals may have a small error when converting.</remarks>
    public BigReal(float value)
    {
      Initialize(value);
    }

    /// <summary>
    /// Initialize a new real number from a decimal value.
    /// </summary>
    /// <param name="value">The decimal value to use.</param>    
    public BigReal(decimal value)
    {
      Initialize(Convert.ToDouble(value));
    }

    /// <summary>
    /// Initialize a new real number from another real number multiplying its denominator by the provided factor.
    /// </summary>
    /// <param name="value">The Real number to use.</param>
    /// <param name="denominatorFactor">The Factor to apply to the new real number.</param>    
    public BigReal(BigReal value, int denominatorFactor)
    {
      Helper.InvalidateNullParam(value, "value");
      Initialize(value.numerator, value.denominator * denominatorFactor);
    }

    /// <summary>
    /// Converts a Real number into a double precision floating point value.
    /// </summary>
    /// <param name="value">The Real number to convert.</param>
    /// <returns>A double precision floating point that represent the provided Real value.</returns>
    public static explicit operator double(BigReal value)
    {
      Helper.InvalidateNullParam(value, "value");
      return (double)value.Numerator / value.Denominator;
    }

    /// <summary>
    /// Converts a double precision floating point value into a Real number.
    /// </summary>
    /// <param name="value">The double precision floating point value.</param>
    /// <returns>An instance of a BigReal.</returns>
    public static implicit operator BigReal(double value)
    {
      return new BigReal(value);
    }

    /// <summary>
    /// Converts a Real number into a Int64 value.
    /// </summary>
    /// <param name="value">The Real number to convert.</param>
    /// <returns>An Int64 that represent the provided Real value.</returns>
    public static explicit operator long(BigReal value)
    {
      Helper.InvalidateNullParam(value, "value");
      return value.Numerator / value.Denominator;
    }

    /// <summary>
    /// Converts a Real number into an Int32 value.
    /// </summary>
    /// <param name="value">The Real number to convert.</param>
    /// <returns>An Int32 that represent the provided Real value.</returns>
    public static explicit operator int(BigReal value)
    {
      Helper.InvalidateNullParam(value, "value");
      return (int)(value.Numerator / value.Denominator);
    }


    /// <summary>
    /// Adds two specified BigReal instances.
    /// </summary>
    /// <param name="left">A BigReal instance.</param>
    /// <param name="right">A BigReal instance.</param>
    /// <returns>A BigReal whose value is the sum of the values of left and right.</returns>
    public static BigReal operator +(BigReal left, BigReal right)
    {
      checked
      {
        Helper.InvalidateNullParam(left, "left");
        Helper.InvalidateNullParam(right, "right");
        return new BigReal(left.Numerator * right.denominator + right.Numerator * left.denominator, left.denominator * right.denominator);
      }
    }

    /// <summary>
    /// Subtracts two specified BigReal instances.
    /// </summary>
    /// <param name="left">A BigReal instance.</param>
    /// <param name="right">A BigReal instance.</param>
    /// <returns>A BigReal whose value is the difference of the values of left and right.</returns>
    public static BigReal operator -(BigReal left, BigReal right)
    {
      checked
      {
        Helper.InvalidateNullParam(left, "left");
        Helper.InvalidateNullParam(right, "right");
        return new BigReal(left.Numerator * right.denominator - right.Numerator * left.denominator, left.denominator * right.denominator);
      }
    }

    /// <summary>
    /// Multiply two specified BigReal instances.
    /// </summary>
    /// <param name="left">A BigReal instance.</param>
    /// <param name="right">A BigReal instance.</param>
    /// <returns>A BigReal whose value is the product of the values of left and right.</returns>
    public static BigReal operator *(BigReal left, BigReal right)
    {
      checked
      {
        Helper.InvalidateNullParam(left, "left");
        Helper.InvalidateNullParam(right, "right");
        return new BigReal(left.Numerator * right.Numerator, left.Denominator * right.Denominator);
      }
    }

    /// <summary>
    /// Divides two specified BigReal instances.
    /// </summary>
    /// <param name="left">A BigReal instance.</param>
    /// <param name="right">A BigReal instance.</param>
    /// <returns>A BigReal whose value is the quotient of the values of left and right.</returns>
    public static BigReal operator /(BigReal left, BigReal right)
    {
      checked
      {
        Helper.InvalidateNullParam(left, "left");
        Helper.InvalidateNullParam(right, "right");
        return new BigReal(left.Numerator * right.Denominator, left.Denominator * right.Numerator);
      }
    }

    /// <summary>
    /// Indicates whether two BigReal instances are equal.
    /// </summary>
    /// <param name="left">A BigReal instance.</param>
    /// <param name="right">A BigReal instance.</param>
    /// <returns>true if the values of left and right are equal; otherwise, false.</returns>
    public static bool operator ==(BigReal left, BigReal right)
    {
      if ((object.Equals(left, null) && !object.Equals(right, null)) || (!object.Equals(left, null) && object.Equals(right, null)))
        return false;

      if (object.Equals(left, null) && object.Equals(right, null))
        return true;

      return left.Numerator == right.Numerator && left.Denominator == right.Denominator;
    }

    /// <summary>
    /// Indicates whether two BigReal instances are not equal.
    /// </summary>
    /// <param name="left">A BigReal instance.</param>
    /// <param name="right">A BigReal instance.</param>
    /// <returns>true if the values of left and right are not equal; otherwise, false.</returns>
    public static bool operator !=(BigReal left, BigReal right)
    {
      return !(left == right);
    }

    /// <summary>
    /// Indicates whether one BigReal instance is larger than the other.
    /// </summary>
    /// <param name="left">A BigReal instance.</param>
    /// <param name="right">A BigReal instance.</param>
    /// <returns>true if the value of left is larger than right; otherwise, false.</returns>
    public static bool operator >(BigReal left, BigReal right)
    {
      return (double)left > (double)right;
    }

    /// <summary>
    /// Indicates whether one BigReal instance is smaller than the other.
    /// </summary>
    /// <param name="left">A BigReal instance.</param>
    /// <param name="right">A BigReal instance.</param>
    /// <returns>true if the value of left is smaller than right; otherwise, false.</returns>
    public static bool operator <(BigReal left, BigReal right)
    {
      return (double)left < (double)right;
    }

    /// <summary>
    /// Indicates whether one BigReal instance is larger or equal to the other.
    /// </summary>
    /// <param name="left">A BigReal instance.</param>
    /// <param name="right">A BigReal instance.</param>
    /// <returns>true if the value of left is larger or equal to right; otherwise, false.</returns>
    public static bool operator >=(BigReal left, BigReal right)
    {
      return (double)left >= (double)right;
    }

    /// <summary>
    /// Indicates whether one BigReal instance is smaller or equal to the other.
    /// </summary>
    /// <param name="left">A BigReal instance.</param>
    /// <param name="right">A BigReal instance.</param>
    /// <returns>true if the value of left is smaller or equal to right; otherwise, false.</returns>
    public static bool operator <=(BigReal left, BigReal right)
    {
      return (double)left <= (double)right;
    }

    /// <summary>
    /// Negate the specified BigReal instance.
    /// </summary>
    /// <param name="item">A BigReal instance.</param>    
    /// <returns>A BigReal whose value is the negative value of item.</returns>
    public static BigReal operator -(BigReal item)
    {
      checked
      {
        Helper.InvalidateNullParam(item, "left");
        return new BigReal(-item.Numerator, item.denominator);
      }
    }


    /// <summary>
    /// Adds two specified BigReal instances.
    /// </summary>
    /// <param name="left">A BigReal instance.</param>
    /// <param name="right">A BigReal instance.</param>
    /// <returns>A BigReal whose value is the sum of the values of left and right.</returns>
    public static BigReal Add(BigReal left, BigReal right)
    {
      return left + right;
    }

    /// <summary>
    /// Subtracts two specified BigReal instances.
    /// </summary>
    /// <param name="left">A BigReal instance.</param>
    /// <param name="right">A BigReal instance.</param>
    /// <returns>A BigReal whose value is the difference of the values of left and right.</returns>
    public static BigReal Subtract(BigReal left, BigReal right)
    {
      return left - right;
    }

    /// <summary>
    /// Multiply two specified BigReal instances.
    /// </summary>
    /// <param name="left">A BigReal instance.</param>
    /// <param name="right">A BigReal instance.</param>
    /// <returns>A BigReal whose value is the product of the values of left and right.</returns>
    public static BigReal Multiply(BigReal left, BigReal right)
    {
      return left * right;
    }

    /// <summary>
    /// Divides two specified BigReal instances.
    /// </summary>
    /// <param name="left">A BigReal instance.</param>
    /// <param name="right">A BigReal instance.</param>
    /// <returns>A BigReal whose value is the quotient of the values of left and right.</returns>
    public static BigReal Divide(BigReal left, BigReal right)
    {
      return left / right;
    }

    /// <summary>
    /// Negate the specified BigReal instance.
    /// </summary>
    /// <param name="item">A BigReal instance.</param>    
    /// <returns>A BigReal whose value is the negative value of item.</returns>
    public static BigReal Negate(BigReal item)
    {
      return -item;
    }


    /// <summary>
    /// Represents a BigReal that has the value of 1.
    /// </summary>
    /// <returns>A BigReal that has the value of 1</returns>
    public static BigReal One()
    {
      return new BigReal(1, 1);
    }

    /// <summary>
    /// Represents a BigReal that has the value of 0 (zero).
    /// </summary>
    /// <returns>A BigReal that has the value of 0 (zero)</returns>
    public static BigReal Zero()
    {
      return new BigReal(0, 1);
    }

    /// <summary>
    /// Represents a BigReal that has the value of -1.
    /// </summary>
    /// <returns>A BigReal that has the value of -1</returns>
    public static BigReal MinusOne()
    {
      return new BigReal(-1, 1);
    }

    private void Initialize(long numerator, long denominator)
    {
      if (denominator < 0)
      {
        numerator = -numerator;
        denominator = -denominator;
      }

      this.numerator = numerator;
      this.denominator = denominator;
      this.Simplify();
    }

    private void Initialize(double value)
    {
      int sign;

      if (value == (long)value)
      {
        numerator = (long)value;
        denominator = 1;
        return;
      }

      if (value < 0)
      {
        sign = -1;
        value = -value;
      }
      else
        sign = 1;


      //if (value < MIN_VALUE)
      //{
      //  numerator = sign;
      //  denominator = (long)(1 / value);
      //}
      //else if (value > MAX_VALUE)
      //{
      //  numerator = (long)value * sign;
      //  denominator = 1;
      //}



      if (value < MIN_VALUE || value > MAX_VALUE)
        throw new ArgumentOutOfRangeException("value", "value is out of range");
      else
      {
        long previousDenominator;
        long tempDenominator;

        double tempValue = value;
        previousDenominator = 0;
        denominator = 1;

        do
        {
          tempValue = 1 / (tempValue - (long)tempValue);
          tempDenominator = denominator;
          denominator = denominator * (long)tempValue + previousDenominator;
          previousDenominator = tempDenominator;
          numerator = (long)(value * denominator + 0.5);
        }
        while (System.Math.Abs(value - (double)numerator / denominator) > RATIO_RESOLUTION && (tempValue != (long)tempValue));

        numerator = sign * numerator;
      }
    }

    private void Simplify()
    {
      long gcd = GreatestCommonDenominator(this.numerator, this.denominator);
      if (gcd > 1)
      {
        this.numerator = this.numerator / gcd;
        this.denominator = this.denominator / gcd;
      }
    }

    private static long GreatestCommonDenominator(long x, long y)
    {
      if (x < 0) x = -x;
      if (y < 0) y = -y;

      if (x == 1 || y == 1) return 1;

      while (x != 0 && y != 0)
      {
        if (x > y)
          x %= y;
        else
          y %= x;
      }

      if (x == 0)
        return y;
      else
        return x;
    }


    #region IComparable<BigReal> Members

    /// <summary>
    ///  Compares the current instance with another object of the same type and returns an integer that indicates whether the current instance precedes, follows,
    ///  or occurs in the same position in the sort order as the other object.
    /// </summary>
    /// <param name="other">A BigReal instance to compare with this instance.</param>
    /// <returns>A value that indicates the relative order of the objects being compared. The return value has these meanings: Value Meaning Less than zero This instance
    /// precedes other in the sort order. Zero This instance occurs in the same position in the sort order as other. Greater than zero This instance follows other in the sort order.
    /// </returns>
    public int CompareTo(BigReal other)
    {
      if (this == other)
        return 0;
      else if (this < other)
        return -1;
      else
        return 1;
    }

    #endregion

    #region IEquatable<BigReal> Members

    /// <summary>
    /// Indicates whether the current object is equal to another object of the BigReal type.
    /// </summary>
    /// <param name="other">A BigReal to compare with this BigReal.</param>
    /// <returns>true if the current BigReal is equal to the other parameter; otherwise, false.</returns>
    public bool Equals(BigReal other)
    {
      return this == other;
    }

    #endregion

    #region IComparable Members
        
    /// <summary>
    /// Compares the current object with another object of the same type.
    /// </summary>
    /// <param name="obj">An object to compare with this object.</param>
    /// <returns>A value that indicates the relative order of the objects being compared. The return value has the following meanings: Value Meaning Less than zero
    /// This object is less than the other parameter.Zero This object is equal to other. Greater than zero This object is greater than other.
    /// </returns>
    public int CompareTo(object obj)
    {
      BigReal bigReal = obj as BigReal;
      if (bigReal == null)
        throw new ArgumentException("obj is not the same type as this instance");
      return this.CompareTo(bigReal);
    }

    #endregion

 
    /// <summary>
    /// Determines whether the specified BigReal is equal to the current BigReal.
    /// </summary>
    /// <param name="obj">The BigReal to compare with the current BigReal.</param>
    /// <returns> true if the specified BigReal is equal to the current BigReal; otherwise, false.</returns>
    public override bool Equals(object obj)
    {
      if (obj == null || GetType() != obj.GetType())
        return false;

      return this == (BigReal)obj;
    }

    /// <summary>
    /// A hash function for the BigReal instance.
    /// </summary>
    /// <returns>A hash code for the current BigReal.</returns>
    public override int GetHashCode()
    {
      return ((double)this).GetHashCode();
    }

    /// <summary>
    /// Returns a string that represents the current BigReal.
    /// </summary>
    /// <returns>A string that represents the current BigReal.</returns>
    public override string ToString()
    {
      long intPart = Numerator / denominator;
      long frac = Numerator % denominator;

      if (intPart == 0)
        return string.Format("{0}/{1}", frac, this.denominator);
      else if (frac == 0)
        return string.Format("{0}", intPart);
      else if (intPart < 0)
        return string.Format("{0} {1}/{2}", intPart, -frac, this.denominator);
      else
        return string.Format("{0} {1}/{2}", intPart, frac, this.denominator);
    }
  }
}